package com.io.nio;

import io.netty.channel.Channel;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

/**
 * NIO 也称 New IO， Non-Block IO，非阻塞同步通信方式 <p>
 * <h2>设计实现</h2>
 * 从BIO的阻塞到NIO的非阻塞，这是一大进步。功归于Buffer，Channel，Selector三个设计实现。<p>
 * <ul>
 * <li>
 * Buffer: 缓冲区。Buffer本身就是一块内存，底层实现上，它实际是一个数组。数据读、写都是通过 Buffer 来实现的。<p>
 * NIO的数据操作都是在缓冲区中进行。而BIO是将数据直接写入或读取到Stream对象。<p>
 * java 中的 7 种原生数据类型都有各自对应的 Buffer 类型。并没有BooleanBuffer类型
 * </li>
 * <li>
 * Channel: 通道。NIO可以通过Channel进行数据的读，写和同时读写操作。<p>
 * 指的是可以向其写入数据或是从中读取数据的对象，它类似于java.io中的Stream <p>
 * 与Stream不同的是，Channel是双向的，一个流只可能是InputStream或是OutputStream，Channel打开后可以进行读取、写入或是读写。
 * </li>
 * <li>
 * Selector: 多路复用器。NIO编程的基础。多路复用器提供选择已经就绪状态任务的能力。
 * </li>
 * </ul>
 * <h2>NIO的特点</h2>
 * 客户端和服务器通过Channel连接，而这些Channel都要注册在Selector。Selector通过一个线程不停的轮询这些Channel。找出已经准备就绪的Channel执行IO操作。<p>
 * NIO通过一个线程轮询，实现千万个客户端的请求，这就是非阻塞NIO的特点。
 */
public class NIOServer implements Runnable {
    private final int BUFFER_SIZE = 1024; // 缓冲区大小
    private final int PORT = 8889;        // 监听的端口
    private Selector selector;            // 多路复用器，NIO编程的基础，负责管理通道Channel
    // 缓冲区Buffer，和BIO的一个重要区别（NIO读写数据是在缓冲区中进行，而BIO是通过流的形式）
    private ByteBuffer readBuffer = ByteBuffer.allocate(BUFFER_SIZE);

    public NIOServer() {
        startServer();
    }

    private void startServer() {
        try {
            // 开启多路复用器
            selector = Selector.open();
            // 打开服务器通道（网络读写通道）
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            // 设置服务器通道为非阻塞模式，true为阻塞，false为非阻塞
            serverSocketChannel.configureBlocking(false);
            // 绑定端口
            serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
            // 把通道注册到多路复用器上，并监听阻塞事件
            /**
             * SelectionKey.OP_READ : 表示关注读数据就绪事件
             * SelectionKey.OP_WRITE : 表示关注写数据就绪事件
             * SelectionKey.OP_CONNECT: 表示关注socket channel的连接完成事件
             * SelectionKey.OP_ACCEPT : 表示关注server-socket channel的accept事件
             */
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("NIO server start >>> port: " + PORT);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    /**
     * 用一个线程负责selector的轮训
     */
    @Override
    public void run() {
        while (true) {
            try {
                // selector监听阻塞
                selector.select();
                // 遍历selector已经选择的结果集
                Iterator<SelectionKey> selectionKey = selector.selectedKeys().iterator();
                while (selectionKey.hasNext()) {
                    // 获取一个选中的key
                    SelectionKey key = selectionKey.next();
                    // 获取后便将其从容器中移除
                    selectionKey.remove();
                    // 只获取有效的key
                    if (!key.isValid()) continue;
                    // 阻塞状态处理
                    if (key.isAcceptable()) accept(key);
                    // 可读状态处理
                    if (key.isReadable()) read(key);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void read(SelectionKey key) {
        SocketChannel socketChannel = null;
        try {
            // 清空缓冲区数据
            readBuffer.clear();
            // 获取在多路复用器上注册的通道
             socketChannel = (SocketChannel) key.channel();
            // 读取数据，返回(将Channel中的数据写入Buffer)
            int count = socketChannel.read(readBuffer);
            // 返回内容为-1 表示没有数据
            if (count == -1) {
                key.channel().close();
                key.cancel();
                return;
            }
            // 有数据则在读取数据前进行复位操作
            readBuffer.flip();
            // 根据缓冲区大小创建一个相应大小的bytes数组，用来获取值
            byte[] bytes = new byte[readBuffer.remaining()];
            // 接收缓冲区数据
            readBuffer.get(bytes);
            System.out.println("NIO Server received : " + new String(bytes));
        } catch (IOException e) {
            e.printStackTrace();
            if (socketChannel != null){
                try {
                    socketChannel.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

    /**
     * 设置阻塞，等待Client请求。<p>
     * 在传统IO编程中，用的是ServerSocket和Socket。在NIO中采用的ServerSocketChannel和SocketChannel
     *
     * @param key
     */
    private void accept(SelectionKey key) {
        try {
            // 获取到通道
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
            // 执行阻塞方法
            SocketChannel socketChannel = serverSocketChannel.accept();
            // 设置服务器通道为非阻塞模式，true为阻塞，false为非阻塞
            socketChannel.configureBlocking(false);
            // 把通道注册到多路复用器上，并设置读取标识
            socketChannel.register(selector, SelectionKey.OP_READ);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    public static void main(String[] args) {
        new Thread(new NIOServer()).start();
    }
}
